# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# TODO(epg): --with-revprop support, but disallow gvn:* setting with
# that mechanism.  Add options to set some gvn: properties
# (e.g. --reviewers => gvn:reviewers --bug => gvn:bug).

import codecs
import os
import posixpath
import sys

import gvn.changebranch
import gvn.cmdline
import gvn.errors
import gvn.platform
import gvn.util
import gvn.wc


helptext__gvn_change = """change: Manage a changebranch.
usage: 1. change [-c CHANGE] [--add] PATH...
       2. change -c CHANGE --remove PATH...
       3. change -c CHANGE --delete

  1. Add PATHs to changebranch, creating it if it does not exist,
     and generating a random changebranch name if none is provided.
  2. Remove PATHs from changebranch.
  3. Delete changebranch.
"""


def _CurrentlyChangeBranched(path, cb):
  for change in cb.changed_paths:
    if gvn.util.IsChild(path, change.relative_path):
      return True
  return False

def _InTargets(path, targets):
  for target in targets:
    if gvn.util.IsChild(path, target):
      return True
  return False


_MESSAGE_DIVIDER = '--All lines above this line become the description--'
_PATHS_IN = 'Paths in this changebranch'
_PATHS_OUT = 'Paths modified but not in any changebranch:'
def ChangeEditor(ctx, cb, desc, action, path_status, changed_paths,
                 editor, encoding):
  """Return (desc, {path: svn_wc_status2_t}) for paths to be changebranched.

  Run the user's editor on a form representing the state of the
  working copy, with desc in first section, cb.changed_paths in
  second, and non-changedbranched modified paths in a third.  Return
  the change description and changed paths from the edited form.

  Arguments:
  ctx           -- gvn.cmdline.Context
  cb            -- gvn.changebranch.ChangeBranch
  desc          -- change description (utf-8 str)
  action        -- action being taken ('add'/None or 'remove')
  path_status   -- dict of all modified paths in the working copy, with
                   svn_wc_status2_t for values
  changed_paths -- paths to be changebranched (utf-8 str keys)
  editor        -- gvn.util.Editor for the user's editor
  encoding      -- user's encoding
  """

  if desc is None:
    desc = ''

  form = [
    desc.rstrip(),
    _MESSAGE_DIVIDER,
    '',
    'Move files between the next two sections to add or remove them from',
    'the changebranch.',
    '',
    '%s (%s):' % (_PATHS_IN, cb.name),
    ]

  # Map display paths back to wc paths.
  display_to_wc = {}

  for wc_path in sorted(changed_paths):
    rel_path = ctx.DisplayPath(wc_path)
    display_to_wc[rel_path] = wc_path

    if _CurrentlyChangeBranched(wc_path, cb):
      moved = ' '
    else:
      moved = '*'
    form.append('%s%s    %s' % (moved,
                                gvn.wc.ActionCode(path_status[wc_path]),
                                rel_path))

  form.append('')
  form.append(_PATHS_OUT)
  form.append('')

  for wc_path in sorted(path_status.iterkeys()):
    if wc_path in changed_paths:
      continue
    if wc_path in ctx.wc.change_state:
      if (ctx.wc.change_state[wc_path].change_name != cb.name
          or action != 'remove'):
        continue
    rel_path = ctx.DisplayPath(wc_path)
    display_to_wc[rel_path] = wc_path

    if _CurrentlyChangeBranched(wc_path, cb):
      moved = '*'
    else:
      moved = ' '
    form.append('%s%s    %s' % (moved,
                                gvn.wc.ActionCode(path_status[wc_path]),
                                rel_path))
  form.append('')

  answer = editor.Edit('\n'.join(form), encoding, tmp_prefix='gvnchange.')
  if answer is None:
    return (None, None)

  state = 0
  result_paths = {}
  desc = []
  for line in answer.splitlines():
    if state == 0:
      if line == _MESSAGE_DIVIDER:
        state = 1
      else:
        desc.append(line)
    elif state == 1 and line.startswith(_PATHS_IN):
      state = 2
    elif state == 2:
      if line == '':
        continue
      elif line == _PATHS_OUT:
        break
      # TODO(epg): Wrong place to be encoding to utf-8; gvn.wc should only
      # return unicode paths, and should .encode all its input; that's not
      # where we are today, though, so have to do this for now.
      path = display_to_wc[line[6:].encode('utf-8')]
      result_paths[path] = path_status[path]

  # Restore final newline.
  desc.append('')

  return (u'\n'.join(desc), result_paths)


def Handle_GvnChange(ctx, editor_class=gvn.util.Editor):

  # TODO(epg): This description block is repeated a few places, i
  # think, along with popping up an editor for the log message.
  # *After* we implement full editor support, see what can be
  # refactored.  Doing it before would just be premature, as we don't
  # know what that's going to look like
  description = None
  if ctx.options.file is not None:
    description = codecs.open(ctx.options.file,
                              encoding=ctx.encoding).read()
  elif ctx.options.message is not None:
    description = ctx.options.message.decode(ctx.encoding)
  # TODO(epg): force-log

  action = None
  if ctx.options.add:
    # Add files on the command line into the change branch.
    if action is not None:
      raise gvn.errors.BadOptions("cannot specify both 'add' and '%s'"
                                   % (action,))
    action = 'add'
  elif ctx.options.delete:
    # Delete the changebranch.
    if action is not None:
      raise gvn.errors.BadOptions("cannot specify both 'delete' and '%s'"
                                   % (action,))
    action = 'delete'
  elif ctx.options.remove:
    # Remove files on the command line from the change branch.
    if action is not None:
      raise gvn.errors.BadOptions("cannot specify both 'remove' and '%s'"
                                   % (action,))
    action = 'remove'

  change_name = ctx.options.change
  if change_name is None:
    if action != 'add' and action is not None:
      # Must specify a change name on which to operate.
      raise gvn.errors.BadOptions("must provide a change name (via '-c')")

    # Generate a unique, random (and short) change name.
    change_name = gvn.changebranch.UniqueChangeName(ctx.project,
                      ctx.project.repository.username,
                      gvn.changebranch.RandomBranchName(),
                      ctx.pool)

  (username, cname, revision) = gvn.util.ParseChangeName(change_name)
  cb = gvn.changebranch.ChangeBranch(ctx.config, ctx.project,
                                     cname, username, revision)
  ctx.ValidateChangeName(cb)

  if action == 'delete':
    cb.Delete(ctx.wc, description, ctx.pool)
    return cb

  wc_paths = set([ctx.path])
  already_changed_paths = set(cb.ChangedPathsRelative())
  if already_changed_paths:
    # Make sure we don't open more deeply than shallowest path on cb
    wc_paths.update(already_changed_paths)
  ctx.wc.Open(wc_paths)

  # Get status of everything from ctx.path down; this is wasted effort
  # in --non-interactive mode...
  paths = {}
  def status_cb(target, status):
    if gvn.wc.IsModified(status):
      paths[ctx.wc.RelativePath(target)] = status
  ctx.wc.Status(wc_paths, recursive=not ctx.options.non_recursive,
                get_all=False, no_ignore=False, show_children=False,
                callback=status_cb)

  if not paths:
    # No modified files, so exit silently like 'svn commit'.
    return cb

  # This is the list of paths to changebranch; initialize it to the
  # list of paths currently on this changebranch, if any.
  changed_paths = already_changed_paths

  # Build the list of paths to add or remove from changed_paths.
  if len(ctx.operands) == 1:
    # If we have only one operand, ctx.path is already it.  TODO(epg):
    # I'm not sure this is the right fix.  See also cmdline.py .
    targets = [ctx.path]
  else:
    targets = [posixpath.join(ctx.path, x) for x in ctx.operands]
  if len(targets) == 0 and not cb.Exists():
    # If changebranch does not exist, add everything
    # (already-changebranched paths are pruned later).
    targets = [ctx.path]

  # Remove all paths in changed_paths starting with any path in targets
  # (e.g. targets=['foo'] changed_paths=['food', 'foo', 'foo/bar'],
  # remove 'foo' and 'foo/bar' but not 'food').
  if action == 'remove':
    for i in list(changed_paths):
      if _InTargets(i, targets):
        changed_paths.remove(i)

  # Add all non-changebranched, locally modified paths starting with
  # any path in targets.
  elif action == 'add' or action is None:
    for i in paths.iterkeys():
      if _InTargets(i, targets) and i not in ctx.wc.change_state:
        changed_paths.add(i)

  if description is None:
    description = cb.description

  editor = None
  try:
    if ctx.options.non_interactive:
      # Set paths to status of changed_paths.
      tmp = {}
      for i in changed_paths:
        try:
          tmp[i] = paths[i]
        except KeyError:
          # User specified unmodified file; ignore it.
          pass
      paths = tmp
    else:
      # Get paths from the user via $EDITOR.
      editor = editor_class(ctx.config.editor_command)
      (description, paths) = ChangeEditor(ctx, cb, description, action,
                                          paths, changed_paths, editor,
                                          ctx.encoding)
      if description is None:
        if not ctx.options.quiet:
          ctx.Notify('cancelled\n')
        return None

    if not paths:
      raise gvn.errors.BadOperands("no pathnames specified!")

    try:
      cb.Branch(ctx.wc, paths, description, ctx.pool)
    except gvn.errors.OutOfDateParent, e:
      e.branch_path = ctx.DisplayPath(e.branch_path)
      raise
    if editor is not None:
      editor.Done()
      editor = None
  finally:
    if editor is not None and not editor.IsDone():
      sys.stderr.write('Your change form was left in a temporary file:\n'
                       '%s\n' % (editor.tmpfile,))

  return cb


def wrap_Handle_GvnChange(ctx):
  if Handle_GvnChange(ctx) is None:
    return 1
  return 0


options = gvn.cmdline.AuthOptions(gvn.cmdline.LogOptions(
  ['add', 'delete', 'remove', 'change', 'force-change',
   'project', 'quiet']))
gvn.cmdline.AddCommand('change', wrap_Handle_GvnChange, helptext__gvn_change,
                       options, {'change': 'changebranch ARG'})
